<?php
namespace WCF\Sniffs\WhiteSpace;

use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Files\File;

/**
 * This sniff is based on Squiz_Sniffs_WhiteSpace_SuperfluousWhitespaceSniff. Originally written
 * by Greg Sherwood <gsherwood@squiz.net> and Marc McIntyre <mmcintyre@squiz.net>
 * and released under the terms of the BSD Licence.
 * See: https://github.com/squizlabs/PHP_CodeSniffer/blob/master/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/SuperfluousWhitespaceSniff.php
 * 
 * @author	Tim Duesterhus
 * @license	GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 * @package	WoltLabSuite\Core
 */
class SuperfluousWhitespaceSniff implements Sniff {
	/**
	 * A list of tokenizers this sniff supports.
	 *
	 * @var array
	 */
	public $supportedTokenizers = array(
		'PHP',
		'JS',
		'CSS',
	);
	
	/**
	 * If TRUE, whitespace rules are not checked for blank lines.
	 *
	 * Blank lines are those that contain only whitespace.
	 *
	 * @var boolean
	 */
	public $ignoreBlankLines = false;
	
	/**
	 * Returns an array of tokens this test wants to listen for.
	 *
	 * @return array
	 */
	public function register() {
		return array(
			T_OPEN_TAG,
			T_CLOSE_TAG,
			T_WHITESPACE,
			T_COMMENT,
		);
	}
	
	/**
	 * Processes this sniff, when one of its tokens is encountered.
	 *
	 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
	 * @param int                  $stackPtr  The position of the current token in the
	 *                                        stack passed in $tokens.
	 */
	public function process(File $phpcsFile, $stackPtr) {
		$tokens = $phpcsFile->getTokens();
		
		if ($tokens[$stackPtr]['code'] === T_OPEN_TAG) {
			// Check for start of file whitespace.
			if ($phpcsFile->tokenizerType !== 'PHP') {
				// The first token is always the open tag inserted when tokenizsed
				// and the second token is always the first piece of content in
				// the file. If the second token is whitespace, there was
				// whitespace at the start of the file.
				if ($tokens[($stackPtr + 1)]['code'] !== T_WHITESPACE) {
					return;
				}
			}
			else {
				// If its the first token, then there is no space.
				if ($stackPtr === 0) {
					return;
				}
				
				for ($i = ($stackPtr - 1); $i >= 0; $i--) {
					// If we find something that isn't inline html then there is something previous in the file.
					if ($tokens[$i]['type'] !== 'T_INLINE_HTML') {
						return;
					}
					
					// If we have ended up with inline html make sure it isn't just whitespace.
					$tokenContent = trim($tokens[$i]['content']);
					if ($tokenContent !== '') {
						return;
					}
				}
			}
		
			$phpcsFile->addError('Additional whitespace found at start of file', $stackPtr, 'StartFile');
		}
		else if ($tokens[$stackPtr]['code'] === T_CLOSE_TAG) {
			// Check for end of file whitespace.
			
			if ($phpcsFile->tokenizerType === 'JS') {
				// The last token is always the close tag inserted when tokenized
				// and the second last token is always the last piece of content in
				// the file. If the second last token is whitespace, there was
				// whitespace at the end of the file.
				if ($tokens[($stackPtr - 1)]['code'] !== T_WHITESPACE) {
					return;
				}
			}
			else if ($phpcsFile->tokenizerType === 'CSS') {
				// The last two tokens are always the close tag and whitespace
				// inserted when tokenizsed and the third last token is always the
				// last piece of content in the file. If the third last token is
				// whitespace, there was whitespace at the end of the file.
				if ($tokens[($stackPtr - 3)]['code'] !== T_WHITESPACE) {
					return;
				}
				
				// Adjust the pointer to give the correct line number for the error.
				$stackPtr -= 2;
			}
			else {
				if (isset($tokens[($stackPtr + 1)]) === false) {
					// The close PHP token is the last in the file.
					return;
				}
				
				for ($i = ($stackPtr + 1); $i < $phpcsFile->numTokens; $i++) {
					// If we find something that isn't inline html then there
					// is more to the file.
					if ($tokens[$i]['type'] !== 'T_INLINE_HTML') {
						return;
					}
					
					// If we have ended up with inline html make sure it
					// isn't just whitespace.
					$tokenContent = trim($tokens[$i]['content']);
					if (empty($tokenContent) === false) {
						return;
					}
				}
			}
			
			$phpcsFile->addError('Additional whitespace found at end of file', $stackPtr, 'EndFile');
		}
		else {
			// Check for end of line whitespace.
			
			// Ignore whitespace that is not at the end of a line.
			if (strpos($tokens[$stackPtr]['content'], $phpcsFile->eolChar) === false) {
				return;
			}
			
			// Ignore blank lines if required.
			if ($this->ignoreBlankLines === true && $tokens[($stackPtr - 1)]['line'] !== $tokens[$stackPtr]['line']) {
				return;
			}
			
			$tokenContent = rtrim($tokens[$stackPtr]['content'], $phpcsFile->eolChar);
			if (empty($tokenContent) === false) {
				if (preg_match("|^.*\n\\s+$|", $tokenContent) !== 0) {
					$phpcsFile->addError('Whitespace found at end of line', $stackPtr, 'EndLine');
				}
			}
		
			// Check for multiple blanks lines in a function.
			
			if ($phpcsFile->hasCondition($stackPtr, T_FUNCTION) === true) {
				if ($tokens[($stackPtr - 1)]['line'] < $tokens[$stackPtr]['line'] && $tokens[($stackPtr - 2)]['line'] === $tokens[($stackPtr - 1)]['line']) {
					// This is an empty line and the line before this one is not
					// empty, so this could be the start of a multiple empty
					// line block.
					$next  = $phpcsFile->findNext(T_WHITESPACE, $stackPtr, null, true);
					$lines = $tokens[$next]['line'] - $tokens[$stackPtr]['line'];
					if ($lines > 1) {
						$error = 'Functions must not contain multiple empty lines in a row; found %s empty lines';
						$data  = array($lines);
						$phpcsFile->addError($error, $stackPtr, 'EmptyLines', $data);
					}
				}
			}
		}
	}
}
